﻿using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;
using Creventive.Wpf.ShellFactory.Services;
using Creventive.Wpf.ShellFactory.Threading;
using Creventive.Wpf.ShellFactory.Utility;
using Creventive.Wpf.ShellFactory.Workspace.Primitives;

namespace Creventive.Wpf.ShellFactory.Workspace
{
    /// <summary>
    ///   Base class for the application view models.
    /// </summary>
    public abstract class ApplicationViewModel : INotifyPropertyChanged
    {
        private readonly ApplicationView view;
        private readonly ApplicationViewModelFlags behaviorFlags;
        private DispatcherThread workerThread;
        private Dispatcher workDispatcher;

        /// <summary>
        ///   Initializes a new instance of the <see cref = "ApplicationViewModel" /> class.
        /// </summary>
        /// <param name = "services">The service container.</param>
        /// <param name = "behaviorFlags">The behavior flags.</param>
        protected ApplicationViewModel(IServiceContainer services, ApplicationViewModelFlags behaviorFlags)
        {
            this.view = services.GetService<ApplicationView>();
            this.behaviorFlags = behaviorFlags;
        }

        /// <summary>
        ///   Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        ///   Gets the flags this view model was constructed with.
        /// </summary>
        /// <value>The flags.</value>
        protected ApplicationViewModelFlags BehaviorFlags
        {
            get { return this.behaviorFlags; }
        }

        /// <summary>
        ///   Gets the service container associated with this view model.
        /// </summary>
        /// <value>The services.</value>
        public IServiceContainer Services
        {
            get { return this.view.Services; }
        }

        /// <summary>
        ///   Gets the view dispatcher.
        /// </summary>
        /// <value>The view dispatcher.</value>
        public Dispatcher ViewDispatcher
        {
            get { return this.view.Dispatcher; }
        }

        /// <summary>
        ///   Gets the work dispatcher. If the UsesWorkerThread flag is specified in the constructor, this dispatcher runs on a separate thread.
        /// </summary>
        /// <value>The work dispatcher.</value>
        public Dispatcher WorkDispatcher
        {
            get
            {
                if (this.workDispatcher == null)
                    throw new InvalidOperationException("The work dispatcher is not available until the OnLoad method is called.");
                return this.workDispatcher;
            }
        }

        internal void Load()
        {
            if (this.view != null && !DesignerProperties.GetIsInDesignMode(this.view))
            {
                if ((this.behaviorFlags & ApplicationViewModelFlags.UsesWorkerThread) == ApplicationViewModelFlags.UsesWorkerThread)
                    this.CreateWorkerThread();
                else if ((this.behaviorFlags & ApplicationViewModelFlags.InheritsWorkerThread) == ApplicationViewModelFlags.InheritsWorkerThread)
                {
                    var parentView = ApplicationView.FindParentView(this.view);
                    while (parentView != null && parentView.ViewModel == null)
                        parentView = ApplicationView.FindParentView(this.view);
                    if (parentView != null)
                        this.workDispatcher = parentView.ViewModel.WorkDispatcher;
                }
                else
                    this.workDispatcher = this.view.Dispatcher;
            }
            else if (this.view != null)
                this.workDispatcher = this.view.Dispatcher;
            else
                this.workDispatcher = null;

            this.OnLoad();
        }

        private void CreateWorkerThread()
        {
            this.workerThread = new DispatcherThread(ApartmentState.STA);
            this.workDispatcher = this.workerThread.Dispatcher;
        }

        /// <summary>
        ///   Called when the view has been loaded and attached, and the view model should load its contents.
        /// </summary>
        protected virtual void OnLoad()
        {}

        /// <summary>
        ///   Usually called before the OnUnload method, allowing the viewmodel to deny the closing of a view. Be aware that there may be some cases where OnCanUnload is NOT called.
        /// </summary>
        /// <returns></returns>
        protected virtual bool OnCanUnload()
        {
            return true;
        }

        /// <summary>
        ///   Called when the view model should unload its content.
        /// </summary>
        protected virtual void OnUnload()
        {
            if (this.workerThread != null)
                this.workerThread.Dispose(true);
        }

        /// <summary>
        ///   Notifies the view that a given set of property names have been changed.
        /// </summary>
        /// <param name = "propertyNames">The property names.</param>
        protected virtual void OnPropertyChanged(params string[] propertyNames)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                if (this.ViewDispatcher.CheckAccess())
                {
                    if (propertyNames == null || propertyNames.Length == 0)
                        handler.Invoke(this, new PropertyChangedEventArgs(string.Empty));
                    else
                        propertyNames.ForEach(name => handler.Invoke(this, new PropertyChangedEventArgs(name)));
                }
                else
                    this.ViewDispatcher.BeginInvoke(new Action<string[]>(this.OnPropertyChanged), propertyNames);
            }
        }

        internal bool CanClose()
        {
            if (!this.OnCanUnload())
                return false;
            return true;
        }

        internal void Close()
        {
            this.OnUnload();
        }
    }
}